Utforsk avanserte teknikker for parallelle datahenting i React ved hjelp av Suspense, som forbedrer applikasjonens ytelse og brukeropplevelse. Lær strategier for å koordinere flere asynkrone operasjoner og håndtere lastetilstander effektivt.
React Suspense-koordinering: Mestring av parallelle datahentingsstrategier
React Suspense har revolusjonert måten vi håndterer asynkrone operasjoner på, spesielt datahenting. Det lar komponenter "suspendere" rendering mens de venter på at data skal lastes inn, og gir en deklarativ måte å håndtere lastetilstander på. Men å bare pakke inn individuelle datahentinger med Suspense kan føre til en fossefall-effekt, der en henting fullføres før den neste starter, noe som negativt påvirker ytelsen. Dette blogginnlegget går i dybden på avanserte strategier for å koordinere flere datahentinger parallelt ved hjelp av Suspense, optimalisere applikasjonens responsivitet og forbedre brukeropplevelsen for et globalt publikum.
Forstå fossefallproblemet ved datahenting
Tenk deg et scenario der du trenger å vise en brukerprofil med navn, avatar og nylig aktivitet. Hvis du henter hver datadel sekvensielt, ser brukeren en lastespinner for navnet, deretter en annen for avataren, og til slutt en for aktivitetsstrømmen. Dette sekvensielle lastemønsteret skaper en fossefall-effekt, som forsinker renderingen av den komplette profilen og frustrerer brukerne. For internasjonale brukere med varierende nettverkshastigheter kan denne forsinkelsen være enda mer uttalt.
Vurder dette forenklede kodeutdraget:
function UserProfile() {
const name = useName(); // Henter brukernavn
const avatar = useAvatar(name); // Henter avatar basert på navn
const activity = useActivity(name); // Henter aktivitet basert på navn
return (
<div>
<h2>{name}</h2>
<img src={avatar} alt="User Avatar" />
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
</div>
);
}
I dette eksemplet er useAvatar og useActivity avhengig av resultatet av useName. Dette skaper en klar fossefall – useAvatar og useActivity kan ikke begynne å hente data før useName er fullført. Dette er ineffektivt og en vanlig flaskehals for ytelsen.
Strategier for parallell datahenting med Suspense
Nøkkelen til å optimalisere datahenting med Suspense er å starte alle dataforespørsler samtidig. Her er flere strategier du kan bruke:
1. Forhåndslasting av data med `React.preload` og ressurser
En av de kraftigste teknikkene er å forhåndslaste data før komponenten i det hele tatt rendres. Dette innebærer å opprette en "ressurs" (et objekt som innkapsler datahentingsløftet) og forhåndshente dataene. `React.preload` hjelper med dette. Når komponenten trenger dataene, er de allerede tilgjengelige, og eliminerer lastetilstanden nesten fullstendig.
Tenk deg en ressurs for å hente et produkt:
const createProductResource = (productId) => {
let promise;
let product;
let error;
const suspender = new Promise((resolve, reject) => {
promise = fetch(`/api/products/${productId}`)
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json();
})
.then(data => {
product = data;
resolve();
})
.catch(e => {
error = e;
reject(e);
});
});
return {
read() {
if (error) {
throw error;
}
if (product) {
return product;
}
throw suspender;
},
};
};
// Bruk:
const productResource = createProductResource(123);
function ProductDetails() {
const product = productResource.read();
return (<div>{product.name}</div>);
}
Nå kan du forhåndslaste denne ressursen før ProductDetails-komponenten rendres. For eksempel under ruteoverganger eller ved sveving.
React.preload(productResource);
Dette sikrer at dataene sannsynligvis er tilgjengelige når ProductDetails-komponenten trenger dem, og minimerer eller eliminerer lastetilstanden.
2. Bruke `Promise.all` for samtidig datahenting
En annen enkel og effektiv tilnærming er å bruke Promise.all til å starte alle datahentinger samtidig innenfor en enkelt Suspense-grense. Dette fungerer bra når dataavhengighetene er kjent på forhånd.
La oss se på eksemplet med brukerprofilen igjen. I stedet for å hente data sekvensielt, kan vi hente navn, avatar og aktivitetsstrøm samtidig:
import { useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
function Name() {
const name = useSuspense(fetchName());
return <h2>{name}</h2>;
}
function Avatar({ name }) {
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity({ name }) {
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const name = useSuspense(fetchName());
return (
<div>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar name={name} />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity name={name} />
</Suspense>
</div>
);
}
export default UserProfile;
Men hvis hver av `Avatar` og `Activity` også er avhengig av `fetchName`, men gjengitt inne i separate suspense-grenser, kan du løfte `fetchName`-løftet til overordnet og gi det via React Context.
import React, { createContext, useContext, useState, useEffect, Suspense } from 'react';
async function fetchName() {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 500));
return 'John Doe';
}
async function fetchAvatar(name) {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 300));
return `https://example.com/avatars/${name.toLowerCase().replace(' ', '-')}.jpg`;
}
async function fetchActivity(name) {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 800));
return [
{ id: 1, text: 'Posted a photo' },
{ id: 2, text: 'Updated profile' },
];
}
function useSuspense(promise) {
const [result, setResult] = useState(null);
useEffect(() => {
let didCancel = false;
promise.then(
(data) => {
if (!didCancel) {
setResult({ status: 'success', value: data });
}
},
(error) => {
if (!didCancel) {
setResult({ status: 'error', value: error });
}
}
);
return () => {
didCancel = true;
};
}, [promise]);
if (result?.status === 'success') {
return result.value;
} else if (result?.status === 'error') {
throw result.value;
} else {
throw promise;
}
}
const NamePromiseContext = createContext(null);
function Avatar() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const avatar = useSuspense(fetchAvatar(name));
return <img src={avatar} alt="User Avatar" />;
}
function Activity() {
const namePromise = useContext(NamePromiseContext);
const name = useSuspense(namePromise);
const activity = useSuspense(fetchActivity(name));
return (
<ul>
{activity.map(item => <li key={item.id}>{item.text}</li>)}
</ul>
);
}
function UserProfile() {
const namePromise = fetchName();
return (
<NamePromiseContext.Provider value={namePromise}>
<Suspense fallback=<div>Loading Avatar...</div>>
<Avatar />
</Suspense>
<Suspense fallback=<div>Loading Activity...</div>>
<Activity />
</Suspense>
</NamePromiseContext.Provider>
);
}
export default UserProfile;
3. Bruke en tilpasset hook for å administrere parallelle hentinger
For mer komplekse scenarier med potensielt betingede dataavhengigheter, kan du opprette en tilpasset hook for å administrere den parallelle datahentingen og returnere en ressurs som Suspense kan bruke.
import { useState, useEffect, useRef } from 'react';
function useParallelData(fetchFunctions) {
const [resource, setResource] = useState(null);
const mounted = useRef(true);
useEffect(() => {
mounted.current = true;
const promises = fetchFunctions.map(fn => fn());
const suspender = Promise.all(promises).then(
(results) => {
if (mounted.current) {
setResource({ status: 'success', value: results });
}
},
(error) => {
if (mounted.current) {
setResource({ status: 'error', value: error });
}
}
);
setResource({
status: 'pending',
value: suspender,
});
return () => {
mounted.current = false;
};
}, [fetchFunctions]);
const read = () => {
if (!resource) {
throw new Error('Resource not yet initialized');
}
if (resource.status === 'pending') {
throw resource.value;
}
if (resource.status === 'error') {
throw resource.value;
}
return resource.value;
};
return { read };
}
// Eksempelbruk:
async function fetchUserData(userId) {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 300));
return { id: userId, name: 'User ' + userId };
}
async function fetchUserPosts(userId) {
// Simuler API-kall
await new Promise(resolve => setTimeout(resolve, 500));
return [{ id: 1, title: 'Post 1' }, { id: 2, title: 'Post 2' }];
}
function UserProfile({ userId }) {
const { read } = useParallelData([
() => fetchUserData(userId),
() => fetchUserPosts(userId),
]);
const [userData, userPosts] = read();
return (
<div>
<h2>{userData.name}</h2>
<ul>
{userPosts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
</div>
);
}
function App() {
return (
<Suspense fallback=<div>Loading user data...</div>>
<UserProfile userId={123} />
</Suspense>
);
}
export default App;
Denne tilnærmingen innkapsler kompleksiteten ved å administrere løftene og lastetilstandene i hooken, noe som gjør komponentkoden renere og mer fokusert på å gjengi dataene.
4. Selektiv hydrering med strømmende serverrendering
For server-rendrerte applikasjoner introduserer React 18 selektiv hydrering med strømmende serverrendering. Dette lar deg sende HTML til klienten i biter etter hvert som den blir tilgjengelig på serveren. Du kan pakke inn saktegående komponenter med <Suspense>-grenser, slik at resten av siden kan bli interaktiv mens de saktegående komponentene fortsatt lastes inn på serveren. Dette forbedrer den opplevde ytelsen dramatisk, spesielt for brukere med trege nettverkstilkoblinger eller enheter.
Tenk deg et scenario der et nyhetsnettsted trenger å vise artikler fra forskjellige regioner i verden (f.eks. Asia, Europa, Amerika). Noen datakilder kan være tregere enn andre. Selektiv hydrering lar deg vise artikler fra raskere regioner først, mens de fra tregere regioner fortsatt lastes inn, og forhindrer at hele siden blokkeres.
Håndtering av feil og lastetilstander
Mens Suspense forenkler administrasjonen av lastetilstander, er feilhåndtering fortsatt avgjørende. Feilgrenser (ved hjelp av componentDidCatch-livssyklusmetoden eller useErrorBoundary-hooken fra biblioteker som `react-error-boundary`) lar deg håndtere feil som oppstår under datahenting eller rendering på en elegant måte. Disse feilrensene bør plasseres strategisk for å fange opp feil innenfor spesifikke Suspense-grenser, og forhindre at hele applikasjonen krasjer.
import React, { Suspense } from 'react';
import { ErrorBoundary } from 'react-error-boundary';
function MyComponent() {
// ... henter data som kan gi feil
}
function App() {
return (
<ErrorBoundary fallback={<div>Noe gikk galt!</div>}>
<Suspense fallback={<div>Laster...</div>}>
<MyComponent />
</Suspense>
</ErrorBoundary>
);
}
Husk å gi informativt og brukervennlig fallback-UI for både laste- og feiltilstander. Dette er spesielt viktig for internasjonale brukere som kan oppleve tregere nettverkshastigheter eller regionale tjenestebrudd.
Beste praksis for å optimalisere datahenting med Suspense
-
<li><b>Identifiser og prioriter kritiske data:</b> Bestem hvilke data som er avgjørende for den første gjengivelsen av applikasjonen din, og prioriter å hente disse dataene først.</li>
<li><b>Forhåndslast data når det er mulig:</b> Bruk `React.preload` og ressurser for å forhåndslaste data før komponenter trenger det, og minimer lastetilstander.</li>
<li><b>Hent data samtidig:</b> Bruk `Promise.all` eller tilpassede hooks for å starte flere datahentinger parallelt.</li>
<li><b>Optimaliser API-endepunkter:</b> Sørg for at API-endepunktene dine er optimalisert for ytelse, og minimerer latens og nyttelaststørrelse. Vurder å bruke teknikker som GraphQL for å bare hente dataene du trenger.</li>
<li><b>Implementer caching:</b> Cache ofte brukte data for å redusere antall API-forespørsler. Vurder å bruke biblioteker som `swr` eller `react-query` for robuste cache-funksjoner.</li>
<li><b>Bruk kodesplitting:</b> Del applikasjonen din i mindre biter for å redusere den første lastetiden. Kombiner kodesplitting med Suspense for å laste inn og gjengi forskjellige deler av applikasjonen din gradvis.</li>
<li><b>Overvåk ytelse:</b> Overvåk applikasjonens ytelse regelmessig ved hjelp av verktøy som Lighthouse eller WebPageTest for å identifisere og adressere ytelsesflaskehalser.</li>
<li><b>Håndter feil på en elegant måte:</b> Implementer feilgrenser for å fange opp feil under datahenting og gjengivelse, og gi informative feilmeldinger til brukerne.</li>
<li><b>Vurder server-side rendering (SSR):</b> Av SEO- og ytelsesmessige årsaker bør du vurdere å bruke SSR med strømming og selektiv hydrering for å levere en raskere innledende opplevelse.</li>
Konklusjon
React Suspense, når det kombineres med strategier for parallell datahenting, gir et kraftig verktøysett for å bygge responsive og ytelsesdyktige webapplikasjoner. Ved å forstå fossefallproblemet og implementere teknikker som forhåndslasting, samtidig henting med Promise.all og tilpassede hooks, kan du forbedre brukeropplevelsen betydelig. Husk å håndtere feil på en elegant måte og overvåke ytelsen for å sikre at applikasjonen din forblir optimalisert for brukere over hele verden. Etter hvert som React fortsetter å utvikle seg, vil utforsking av nye funksjoner som selektiv hydrering med strømmende serverrendering ytterligere forbedre din evne til å levere eksepsjonelle brukeropplevelser, uavhengig av sted eller nettverksforhold. Ved å omfavne disse teknikkene kan du lage applikasjoner som ikke bare er funksjonelle, men også en glede å bruke for ditt globale publikum.
Dette blogginnlegget har som mål å gi en omfattende oversikt over parallelle datahentingsstrategier med React Suspense. Vi håper du fant det informativt og nyttig. Vi oppfordrer deg til å eksperimentere med disse teknikkene i dine egne prosjekter og dele funnene dine med fellesskapet.